(in-package :cl-gcalc)

(defvar *api-endpoint* "http://www.google.com/search"
  "API endpoint.")

(defvar *result-pattern* (create-scanner "(?<=\\<h2 class=r style=\"font-size:138%\"><b>).+(?=</b></h2>)")
  "Regex to extract Google Calculator widget form search results.")

(define-condition gcalc-invalid-expression (error) ()
  (:report "Invalid calculator expression."))

(defun extract-result (result-page)
  "Extract Google Calculator widget form RESULTS-PAGE and 
   return the right-hand side of the equation."
  (or (second (split " = " (scan-to-strings *result-pattern* result-page) 
                     :limit 2))
      (error 'gcalc-invalid-expression)))

(defun preprocess-result (result)
  "Remove pretty formatting."
  (labels ((->scientific-expt (result)
             (regex-replace-all " &#215; 10<sup>(-?\\d+)</sup>" result "e\\1"))
           (group-digits (result)
             (regex-replace-all #.(format nil "(\\d+)(?:<font size=-2>)?~
                                               [~A\\s](?:</font>)?(\\d)"
                                          #\NO-BREAK_SPACE) 
                                result 
                                "\\1\\2")))
    (->scientific-expt (group-digits result))))
      
(defun parse-result (result)
  "Make RESULT READ-able."
  (let ((number-class "(?:[#0][xbo])?[+-]?[\\da-f]+(?:\\.\\d+)?(?:e-?\\d+)?"))
    (labels ((ensure-sign (result)
              (regex-replace "^(#[xbo])?([\\da-f]+)" result "\\1+\\2"))
             (ensure-realpart (result)
               (regex-replace (format nil "^(#[xbo])?([+-])(~A i\\s|$)" 
                                      number-class) 
                              (ensure-sign result) 
                              "0 \\2 \\1\\3"))
             (parse-complex (result)
               (regex-replace (format nil "(~A) ([+-]) (#[xbo])?(~A) i" 
                                      number-class number-class) 
                              (ensure-realpart result)
                              "#C(\\1 \\3\\2\\4)"))
             (->sharpsign-base (result)
               (regex-replace-all "0(?=[xbo])" result "#")))
      (parse-complex (->sharpsign-base result)))))

(defun read-from-result (result)
  "READ RESULT as a NUMBER, units pair."
  (let ((result (parse-result (preprocess-result result))))
    (multiple-value-bind (value units-start) (read-from-string result)
      (values value (subseq result units-start)))))

(defun gcalc-raw (expression)
  "Evaluate EXPRESSION with Google Calculator."
  (extract-result (http-request *api-endpoint* 
                                :method :get 
                                :parameters `(("q" . ,expression)))))
     
(defun gcalc (expression)
  "Evaluate EXPRESSION with Google Calculator and parse the result."
  (read-from-result (gcalc-raw expression)))
          
(defun make-gcalc-closure (expression)
  "Make Google Calculator EXPRESSION with free variables into a function."
  (lambda (&rest arguments) (gcalc (apply #'format nil expression arguments))))

(defun gcalc-repl ()
  "Google Calculator REPL."
  (format t "~2%Google Calculator REPL~%Type q to quit.~2%")
  (let ((history (make-array 0 :fill-pointer t :adjustable t)))
    (labels ((expand-history (match history-index)
               (declare (ignore match))
               (aref history (if (plusp (length history-index))
                                 (parse-integer history-index)
                                 (1- (length history)))))
             (read-expression ()
               (format t "> ")
               (finish-output)
               (regex-replace-all "#(\\d*)" (read-line) #'expand-history 
                                  :simple-calls t)))
      (loop :for expression = (read-expression)
            :until (string-equal expression "q")
            :do (handler-case
                  (let ((result (preprocess-result (gcalc-raw expression))))
                    (format t "#~A= ~A~%" (vector-push-extend result history) 
                                          result))
                  (gcalc-invalid-expression (condition) 
                    (format t "~A~%" condition)))))))
